import { hasOwn } from '../utils'
import Node from './node'
import { getNodeKey } from './util'

import type {
    FilterNodeMethodFunction,
    FilterValue,
    LoadFunction,
    TreeData,
    TreeKey,
    TreeNodeData,
    TreeOptionProps,
    TreeStoreNodesMap,
    TreeStoreOptions,
} from '../tree.type'

export default class TreeStore {
    currentNode: Node | any
    currentNodeKey: TreeKey | any
    nodesMap: TreeStoreNodesMap
    root: Node | any
    data: TreeData | any
    lazy: boolean | any
    load: LoadFunction | any
    filterNodeMethod: FilterNodeMethodFunction | any
    key: TreeKey | any
    defaultCheckedKeys: TreeKey[] | any
    checkStrictly: boolean | any
    defaultExpandedKeys: TreeKey[] | any
    autoExpandParent: boolean | any
    defaultExpandAll: boolean | any
    checkDescendants: boolean | any
    props: TreeOptionProps | any

    constructor(options: TreeStoreOptions) {
        this.currentNode = null
        this.currentNodeKey = null

        for (const option in options) {
            if (hasOwn(options, option)) {
                this[option] = options[option]
            }
        }

        this.nodesMap = {}
    }

    initialize() {
        this.root = new Node({
            data: this.data,
            store: this,
        })
        this.root.initialize()

        if (this.lazy && this.load) {
            const loadFn = this.load
            loadFn(this.root, (data: any) => {
                this.root.doCreateChildren(data)
                this._initDefaultCheckedNodes()
            })
        } else {
            this._initDefaultCheckedNodes()
        }
    }

    filter(value: FilterValue): void {
        const filterNodeMethod = this.filterNodeMethod
        const lazy = this.lazy
        const traverse = function (node: TreeStore | Node) {
            const childNodes = (node as TreeStore).root ? (node as TreeStore).root.childNodes : (node as Node).childNodes

            childNodes.forEach((child: any) => {
                child.visible = filterNodeMethod.call(child, value, child.data, child)

                traverse(child)
            })

            if (!(node as Node).visible && childNodes.length) {
                let allHidden = true
                allHidden = !childNodes.some((child: any) => child.visible)

                if ((node as TreeStore).root) {
                    ;(node as TreeStore).root.visible = allHidden === false
                } else {
                    ;(node as Node).visible = allHidden === false
                }
            }
            if (!value) return

            if ((node as Node).visible && !(node as Node).isLeaf && !lazy) (node as Node).expand()
        }

        traverse(this)
    }

    setData(newVal: TreeData): void {
        const instanceChanged = newVal !== this.root.data
        if (instanceChanged) {
            this.root.setData(newVal)
            this._initDefaultCheckedNodes()
        } else {
            this.root.updateChildren()
        }
    }

    getNode(data: TreeKey | TreeNodeData): Node {
        if (data instanceof Node) return data
        const key = typeof data !== 'object' ? data : getNodeKey(this.key, data)
        return this.nodesMap[key] || null
    }

    insertBefore(data: TreeNodeData, refData: TreeKey | TreeNodeData): void {
        const refNode = this.getNode(refData)
        refNode.parent.insertBefore({ data }, refNode)
    }

    insertAfter(data: TreeNodeData, refData: TreeKey | TreeNodeData): void {
        const refNode = this.getNode(refData)
        refNode.parent.insertAfter({ data }, refNode)
    }

    remove(data: TreeNodeData | Node): void {
        const node = this.getNode(data)

        if (node && node.parent) {
            if (node === this.currentNode) {
                this.currentNode = null
            }
            node.parent.removeChild(node)
        }
    }

    append(data: TreeNodeData, parentData: TreeNodeData | TreeKey | Node): void {
        const parentNode = parentData ? this.getNode(parentData) : this.root

        if (parentNode) {
            parentNode.insertChild({ data })
        }
    }

    _initDefaultCheckedNodes(): void {
        const defaultCheckedKeys = this.defaultCheckedKeys || []
        const nodesMap = this.nodesMap

        defaultCheckedKeys.forEach((checkedKey: any) => {
            const node = nodesMap[checkedKey]

            if (node) {
                node.setChecked(true, !this.checkStrictly)
            }
        })
    }

    _initDefaultCheckedNode(node: Node): void {
        const defaultCheckedKeys = this.defaultCheckedKeys || []

        if (defaultCheckedKeys.includes(node.key)) {
            node.setChecked(true, !this.checkStrictly)
        }
    }

    setDefaultCheckedKey(newVal: TreeKey[]): void {
        if (newVal !== this.defaultCheckedKeys) {
            this.defaultCheckedKeys = newVal
            this._initDefaultCheckedNodes()
        }
    }

    registerNode(node: Node): void {
        const key = this.key
        if (!node || !node.data) return

        if (!key) {
            this.nodesMap[node.id] = node
        } else {
            const nodeKey = node.key
            if (nodeKey !== undefined) this.nodesMap[node.key] = node
        }
    }

    deregisterNode(node: Node): void {
        const key = this.key
        if (!key || !node || !node.data) return

        node.childNodes.forEach((child) => {
            this.deregisterNode(child)
        })

        delete this.nodesMap[node.key]
    }

    getCheckedNodes(leafOnly = false, includeHalfChecked = false): TreeNodeData[] {
        const checkedNodes: TreeNodeData[] = []
        const traverse = function (node: TreeStore | Node) {
            const childNodes = (node as TreeStore).root ? (node as TreeStore).root.childNodes : (node as Node).childNodes

            childNodes.forEach((child: any) => {
                if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) {
                    checkedNodes.push(child.data)
                }

                traverse(child)
            })
        }

        traverse(this)

        return checkedNodes
    }

    getCheckedKeys(leafOnly = false): TreeKey[] {
        return this.getCheckedNodes(leafOnly).map((data) => (data || {})[this.key])
    }

    getHalfCheckedNodes(): TreeNodeData[] {
        const nodes: TreeNodeData[] = []
        const traverse = function (node: TreeStore | Node) {
            const childNodes = (node as TreeStore).root ? (node as TreeStore).root.childNodes : (node as Node).childNodes

            childNodes.forEach((child: any) => {
                if (child.indeterminate) {
                    nodes.push(child.data)
                }

                traverse(child)
            })
        }

        traverse(this)

        return nodes
    }

    getHalfCheckedKeys(): TreeKey[] {
        return this.getHalfCheckedNodes().map((data) => (data || {})[this.key])
    }

    _getAllNodes(): Node[] {
        const allNodes: Node[] = []
        const nodesMap = this.nodesMap
        for (const nodeKey in nodesMap) {
            if (hasOwn(nodesMap, nodeKey)) {
                allNodes.push(nodesMap[nodeKey])
            }
        }

        return allNodes
    }

    updateChildren(key: TreeKey, data: TreeData): void {
        const node: any = this.nodesMap[key]
        if (!node) return
        const childNodes = node.childNodes
        for (let i = childNodes.length - 1; i >= 0; i--) {
            const child: any = childNodes[i]
            this.remove(child.data)
        }
        for (let i = 0, j = data.length; i < j; i++) {
            const child = data[i]
            this.append(child, node.data)
        }
    }

    _setCheckedKeys(key: TreeKey, leafOnly = false, checkedKeys: { [key: string]: boolean }): void {
        const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level)
        const cache = Object.create(null)
        const keys = Object.keys(checkedKeys)
        allNodes.forEach((node) => node.setChecked(false, false))
        for (let i = 0, j = allNodes.length; i < j; i++) {
            const node: any = allNodes[i]
            const nodeKey = node.data[key].toString()
            const checked = keys.includes(nodeKey)
            if (!checked) {
                if (node.checked && !cache[nodeKey]) {
                    node.setChecked(false, false)
                }
                continue
            }

            let parent = node.parent
            while (parent && parent.level > 0) {
                cache[parent.data[key]] = true
                parent = parent.parent
            }

            if (node.isLeaf || this.checkStrictly) {
                node.setChecked(true, false)
                continue
            }
            node.setChecked(true, true)

            if (leafOnly) {
                node.setChecked(false, false)
                const traverse = function (node: Node): void {
                    const childNodes = node.childNodes
                    childNodes.forEach((child) => {
                        if (!child.isLeaf) {
                            child.setChecked(false, false)
                        }
                        traverse(child)
                    })
                }
                traverse(node)
            }
        }
    }

    setCheckedNodes(array: Node[], leafOnly = false): void {
        const key = this.key
        const checkedKeys = {} as any
        array.forEach((item) => {
            const _key: any = item || {}
            checkedKeys[_key[key]] = true
        })

        this._setCheckedKeys(key, leafOnly, checkedKeys)
    }

    setCheckedKeys(keys: TreeKey[], leafOnly = false): void {
        this.defaultCheckedKeys = keys
        const key = this.key
        const checkedKeys: any = {}
        keys.forEach((key) => {
            checkedKeys[key] = true
        })

        this._setCheckedKeys(key, leafOnly, checkedKeys)
    }

    setDefaultExpandedKeys(keys: TreeKey[]) {
        keys = keys || []
        this.defaultExpandedKeys = keys
        keys.forEach((key) => {
            const node = this.getNode(key)
            if (node) node.expand(undefined, this.autoExpandParent)
        })
    }

    setChecked(data: TreeKey | TreeNodeData, checked: boolean, deep: boolean): void {
        const node = this.getNode(data)

        if (node) {
            node.setChecked(!!checked, deep)
        }
    }

    getCurrentNode(): Node {
        return this.currentNode
    }

    setCurrentNode(currentNode: Node): void {
        const prevCurrentNode = this.currentNode
        if (prevCurrentNode) {
            prevCurrentNode.isCurrent = false
        }
        this.currentNode = currentNode
        this.currentNode.isCurrent = true
    }

    setUserCurrentNode(node: Node | any, shouldAutoExpandParent = true): void {
        const key = node[this.key]
        const currNode = this.nodesMap[key]
        this.setCurrentNode(currNode)
        if (shouldAutoExpandParent && this.currentNode.level > 1) {
            this.currentNode.parent.expand(null, true)
        }
    }

    setCurrentNodeKey(key: TreeKey, shouldAutoExpandParent = true): void {
        if (key === null || key === undefined) {
            this.currentNode && (this.currentNode.isCurrent = false)
            this.currentNode = null
            return
        }
        const node = this.getNode(key)
        if (node) {
            this.setCurrentNode(node)
            if (shouldAutoExpandParent && this.currentNode.level > 1) {
                this.currentNode.parent.expand(null, true)
            }
        }
    }
}
